#include "pch.h"

#include <vcpkg/base/checks.h>
#include <vcpkg/base/expected.h>
#include <vcpkg/base/files.h>
#include <vcpkg/base/system.print.h>
#include <vcpkg/base/system.process.h>
#include <vcpkg/base/util.h>
#include <vcpkg/commands.h>
#include <vcpkg/metrics.h>
#include <vcpkg/userconfig.h>

namespace vcpkg::Commands::Integrate
{
#if defined(_WIN32)
    static std::string create_appdata_targets_shortcut(const std::string& target_path) noexcept
    {
        return Strings::format(R"###(
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Condition="Exists('%s') and '$(VCPkgLocalAppDataDisabled)' == ''" Project="%s" />
</Project>
)###",
                               target_path,
                               target_path);
    }
#endif

#if defined(_WIN32)
    static std::string create_system_targets_shortcut() noexcept
    {
        return R"###(
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- version 1 -->
  <PropertyGroup>
    <VCLibPackagePath Condition="'$(VCLibPackagePath)' == ''">$(LOCALAPPDATA)\vcpkg\vcpkg.user</VCLibPackagePath>
  </PropertyGroup>
  <Import Condition="'$(VCLibPackagePath)' != '' and Exists('$(VCLibPackagePath).targets')" Project="$(VCLibPackagePath).targets" />
</Project>
)###";
    }
#endif

#if defined(_WIN32)
    static std::string create_nuget_targets_file_contents(const fs::path& msbuild_vcpkg_targets_file) noexcept
    {
        const std::string as_string = msbuild_vcpkg_targets_file.string();

        return Strings::format(R"###(
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="%s" Condition="Exists('%s')" />
  <Target Name="CheckValidPlatform" BeforeTargets="Build">
    <Error Text="Unsupported architecture combination. Remove the 'vcpkg' nuget package." Condition="'$(VCPkgEnabled)' != 'true' and '$(VCPkgDisableError)' == ''"/>
  </Target>
</Project>
)###",
                               as_string,
                               as_string);
    }
#endif

#if defined(_WIN32)
    static std::string create_nuget_props_file_contents() noexcept
    {
        return R"###(
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <VCPkgLocalAppDataDisabled>true</VCPkgLocalAppDataDisabled>
  </PropertyGroup>
</Project>
)###";
    }
#endif

#if defined(_WIN32)
    static std::string get_nuget_id(const fs::path& vcpkg_root_dir)
    {
        std::string dir_id = vcpkg_root_dir.generic_u8string();
        std::replace(dir_id.begin(), dir_id.end(), '/', '.');
        dir_id.erase(1, 1); // Erasing the ":"

        // NuGet id cannot have invalid characters. We will only use alphanumeric and dot.
        Util::erase_remove_if(dir_id, [](char c) { return !isalnum(c) && (c != '.'); });

        const std::string nuget_id = "vcpkg." + dir_id;
        return nuget_id;
    }
#endif

#if defined(_WIN32)
    static std::string create_nuspec_file_contents(const fs::path& vcpkg_root_dir,
                                                   const std::string& nuget_id,
                                                   const std::string& nupkg_version)
    {
        static constexpr auto CONTENT_TEMPLATE = R"(
<package>
    <metadata>
        <id>@NUGET_ID@</id>
        <version>@VERSION@</version>
        <authors>vcpkg</authors>
        <description>
            This package imports all libraries currently installed in @VCPKG_DIR@. This package does not contain any libraries and instead refers to the folder directly (like a symlink).
        </description>
    </metadata>
    <files>
        <file src="vcpkg.nuget.props" target="build\native\@NUGET_ID@.props" />
        <file src="vcpkg.nuget.targets" target="build\native\@NUGET_ID@.targets" />
    </files>
</package>
)";

        std::string content = Strings::replace_all(CONTENT_TEMPLATE, "@NUGET_ID@", nuget_id);
        content = Strings::replace_all(std::move(content), "@VCPKG_DIR@", vcpkg_root_dir.string());
        content = Strings::replace_all(std::move(content), "@VERSION@", nupkg_version);
        return content;
    }
#endif

#if defined(_WIN32)
    enum class ElevationPromptChoice
    {
        YES,
        NO
    };

    static ElevationPromptChoice elevated_cmd_execute(const std::string& param)
    {
        SHELLEXECUTEINFOW sh_ex_info{};
        sh_ex_info.cbSize = sizeof(sh_ex_info);
        sh_ex_info.fMask = SEE_MASK_NOCLOSEPROCESS;
        sh_ex_info.hwnd = nullptr;
        sh_ex_info.lpVerb = L"runas";
        sh_ex_info.lpFile = L"cmd"; // Application to start

        auto wparam = Strings::to_utf16(param);
        sh_ex_info.lpParameters = wparam.c_str(); // Additional parameters
        sh_ex_info.lpDirectory = nullptr;
        sh_ex_info.nShow = SW_HIDE;
        sh_ex_info.hInstApp = nullptr;

        if (!ShellExecuteExW(&sh_ex_info))
        {
            return ElevationPromptChoice::NO;
        }
        if (sh_ex_info.hProcess == nullptr)
        {
            return ElevationPromptChoice::NO;
        }
        WaitForSingleObject(sh_ex_info.hProcess, INFINITE);
        CloseHandle(sh_ex_info.hProcess);
        return ElevationPromptChoice::YES;
    }
#endif

#if defined(_WIN32)
    static fs::path get_appdata_targets_path()
    {
        static const fs::path LOCAL_APP_DATA =
            fs::u8path(System::get_environment_variable("LOCALAPPDATA").value_or_exit(VCPKG_LINE_INFO));
        return LOCAL_APP_DATA / "vcpkg" / "vcpkg.user.targets";
    }
#endif

    static fs::path get_path_txt_path() { return get_user_dir() / "vcpkg.path.txt"; }

#if defined(_WIN32)
    static void integrate_install_msbuild14(Files::Filesystem& fs, const fs::path& tmp_dir)
    {
        static const std::array<fs::path, 2> OLD_SYSTEM_TARGET_FILES = {
            System::get_program_files_32_bit().value_or_exit(VCPKG_LINE_INFO) /
                "MSBuild/14.0/Microsoft.Common.Targets/ImportBefore/vcpkg.nuget.targets",
            System::get_program_files_32_bit().value_or_exit(VCPKG_LINE_INFO) /
                "MSBuild/14.0/Microsoft.Common.Targets/ImportBefore/vcpkg.system.targets"};
        static const fs::path SYSTEM_WIDE_TARGETS_FILE =
            System::get_program_files_32_bit().value_or_exit(VCPKG_LINE_INFO) /
            "MSBuild/Microsoft.Cpp/v4.0/V140/ImportBefore/Default/vcpkg.system.props";

        // TODO: This block of code should eventually be removed
        for (auto&& old_system_wide_targets_file : OLD_SYSTEM_TARGET_FILES)
        {
            if (fs.exists(old_system_wide_targets_file))
            {
                const std::string param =
                    Strings::format(R"(/c DEL "%s" /Q > nul)", old_system_wide_targets_file.string());
                const ElevationPromptChoice user_choice = elevated_cmd_execute(param);
                switch (user_choice)
                {
                    case ElevationPromptChoice::YES: break;
                    case ElevationPromptChoice::NO:
                        System::print2(System::Color::warning, "Warning: Previous integration file was not removed\n");
                        Checks::exit_fail(VCPKG_LINE_INFO);
                    default: Checks::unreachable(VCPKG_LINE_INFO);
                }
            }
        }
        bool should_install_system = true;
        const Expected<std::string> system_wide_file_contents = fs.read_contents(SYSTEM_WIDE_TARGETS_FILE);
        static const std::regex RE(R"###(<!-- version (\d+) -->)###");
        if (const auto contents_data = system_wide_file_contents.get())
        {
            std::match_results<std::string::const_iterator> match;
            const auto found = std::regex_search(*contents_data, match, RE);
            if (found)
            {
                const int ver = atoi(match[1].str().c_str());
                if (ver >= 1) should_install_system = false;
            }
        }

        if (should_install_system)
        {
            const fs::path sys_src_path = tmp_dir / "vcpkg.system.targets";
            fs.write_contents(sys_src_path, create_system_targets_shortcut(), VCPKG_LINE_INFO);

            const std::string param = Strings::format(R"(/c mkdir "%s" & copy "%s" "%s" /Y > nul)",
                                                      SYSTEM_WIDE_TARGETS_FILE.parent_path().string(),
                                                      sys_src_path.string(),
                                                      SYSTEM_WIDE_TARGETS_FILE.string());
            const ElevationPromptChoice user_choice = elevated_cmd_execute(param);
            switch (user_choice)
            {
                case ElevationPromptChoice::YES: break;
                case ElevationPromptChoice::NO:
                    System::print2(System::Color::warning, "Warning: integration was not applied\n");
                    Checks::exit_fail(VCPKG_LINE_INFO);
                default: Checks::unreachable(VCPKG_LINE_INFO);
            }

            Checks::check_exit(VCPKG_LINE_INFO,
                               fs.exists(SYSTEM_WIDE_TARGETS_FILE),
                               "Error: failed to copy targets file to %s",
                               SYSTEM_WIDE_TARGETS_FILE.string());
        }
    }
#endif

    static void integrate_install(const VcpkgPaths& paths)
    {
        auto& fs = paths.get_filesystem();

#if defined(_WIN32)
        {
            std::error_code ec;
            const fs::path tmp_dir = paths.buildsystems / "tmp";
            fs.create_directory(paths.buildsystems, ec);
            fs.create_directory(tmp_dir, ec);

            integrate_install_msbuild14(fs, tmp_dir);

            const fs::path appdata_src_path = tmp_dir / "vcpkg.user.targets";
            fs.write_contents(appdata_src_path,
                              create_appdata_targets_shortcut(paths.buildsystems_msbuild_targets.u8string()),
                              VCPKG_LINE_INFO);
            auto appdata_dst_path = get_appdata_targets_path();

            const auto rc = fs.copy_file(appdata_src_path, appdata_dst_path, fs::copy_options::overwrite_existing, ec);

            if (!rc || ec)
            {
                System::print2(System::Color::error,
                               "Error: Failed to copy file: ",
                               appdata_src_path.u8string(),
                               " -> ",
                               appdata_dst_path.u8string(),
                               "\n");
                Checks::exit_fail(VCPKG_LINE_INFO);
            }
        }
#endif

        const auto pathtxt = get_path_txt_path();
        std::error_code ec;
        fs.write_contents(pathtxt, paths.root.generic_u8string(), VCPKG_LINE_INFO);

        System::print2(System::Color::success, "Applied user-wide integration for this vcpkg root.\n");
        const fs::path cmake_toolchain = paths.buildsystems / "vcpkg.cmake";
#if defined(_WIN32)
        System::printf(
            R"(
All MSBuild C++ projects can now #include any installed libraries.
Linking will be handled automatically.
Installing new libraries will make them instantly available.

CMake projects should use: "-DCMAKE_TOOLCHAIN_FILE=%s"
)",
            cmake_toolchain.generic_u8string());
#else
        System::printf(
            R"(
CMake projects should use: "-DCMAKE_TOOLCHAIN_FILE=%s"
)",
            cmake_toolchain.generic_u8string());
#endif

        Checks::exit_success(VCPKG_LINE_INFO);
    }

    static void integrate_remove(Files::Filesystem& fs)
    {
        std::error_code ec;
        bool was_deleted = false;

#if defined(_WIN32)
        const fs::path path = get_appdata_targets_path();

        was_deleted |= fs.remove(path, ec);
        Checks::check_exit(VCPKG_LINE_INFO, !ec, "Error: Unable to remove user-wide integration: %s", ec.message());
#endif

        was_deleted |= fs.remove(get_path_txt_path(), ec);
        Checks::check_exit(VCPKG_LINE_INFO, !ec, "Error: Unable to remove user-wide integration: %s", ec.message());

        if (was_deleted)
        {
            System::print2(System::Color::success, "User-wide integration was removed\n");
        }
        else
        {
            System::print2(System::Color::success, "User-wide integration is not installed\n");
        }

        Checks::exit_success(VCPKG_LINE_INFO);
    }

#if defined(WIN32)
    static void integrate_project(const VcpkgPaths& paths)
    {
        auto& fs = paths.get_filesystem();

        const fs::path& nuget_exe = paths.get_tool_exe(Tools::NUGET);

        const fs::path& buildsystems_dir = paths.buildsystems;
        const fs::path tmp_dir = buildsystems_dir / "tmp";
        std::error_code ec;
        fs.create_directory(buildsystems_dir, ec);
        fs.create_directory(tmp_dir, ec);

        const fs::path targets_file_path = tmp_dir / "vcpkg.nuget.targets";
        const fs::path props_file_path = tmp_dir / "vcpkg.nuget.props";
        const fs::path nuspec_file_path = tmp_dir / "vcpkg.nuget.nuspec";
        const std::string nuget_id = get_nuget_id(paths.root);
        const std::string nupkg_version = "1.0.0";

        fs.write_contents(
            targets_file_path, create_nuget_targets_file_contents(paths.buildsystems_msbuild_targets), VCPKG_LINE_INFO);
        fs.write_contents(props_file_path, create_nuget_props_file_contents(), VCPKG_LINE_INFO);
        fs.write_contents(
            nuspec_file_path, create_nuspec_file_contents(paths.root, nuget_id, nupkg_version), VCPKG_LINE_INFO);

        // Using all forward slashes for the command line
        const std::string cmd_line = Strings::format(R"("%s" pack -OutputDirectory "%s" "%s" > nul)",
                                                     nuget_exe.u8string(),
                                                     buildsystems_dir.u8string(),
                                                     nuspec_file_path.u8string());

        const int exit_code = System::cmd_execute_clean(cmd_line);

        const fs::path nuget_package = buildsystems_dir / Strings::format("%s.%s.nupkg", nuget_id, nupkg_version);
        Checks::check_exit(
            VCPKG_LINE_INFO, exit_code == 0 && fs.exists(nuget_package), "Error: NuGet package creation failed");
        System::print2(System::Color::success, "Created nupkg: ", nuget_package.u8string(), '\n');

        auto source_path = buildsystems_dir.u8string();
        source_path = Strings::replace_all(std::move(source_path), "`", "``");

        System::printf(R"(
With a project open, go to Tools->NuGet Package Manager->Package Manager Console and paste:
    Install-Package %s -Source "%s"

)",
                       nuget_id,
                       source_path);

        Checks::exit_success(VCPKG_LINE_INFO);
    }
#endif

#if defined(_WIN32)
    static void integrate_powershell(const VcpkgPaths& paths)
    {
        static constexpr StringLiteral TITLE = "PowerShell Tab-Completion";
        const fs::path script_path = paths.scripts / "addPoshVcpkgToPowershellProfile.ps1";

        const auto& ps = paths.get_tool_exe("powershell-core");
        const std::string cmd = Strings::format(
            R"("%s" -NoProfile -ExecutionPolicy Bypass -Command "& {& '%s' }")", ps.u8string(), script_path.u8string());
        const int rc = System::cmd_execute(cmd);
        if (rc)
        {
            System::printf(System::Color::error,
                           "%s\n"
                           "Could not run:\n"
                           "    '%s'\n",
                           TITLE,
                           script_path.generic_u8string());

            {
                auto locked_metrics = Metrics::g_metrics.lock();
                locked_metrics->track_property("error", "powershell script failed");
                locked_metrics->track_property("title", TITLE);
            }
        }

        Checks::exit_with_code(VCPKG_LINE_INFO, rc);
    }
#else
    static void integrate_bash(const VcpkgPaths& paths)
    {
        const auto home_path = System::get_environment_variable("HOME").value_or_exit(VCPKG_LINE_INFO);
        const fs::path bashrc_path = fs::path{home_path} / ".bashrc";

        auto& fs = paths.get_filesystem();
        const fs::path completion_script_path = paths.scripts / "vcpkg_completion.bash";

        Expected<std::vector<std::string>> maybe_bashrc_content = fs.read_lines(bashrc_path);
        Checks::check_exit(
            VCPKG_LINE_INFO, maybe_bashrc_content.has_value(), "Unable to read %s", bashrc_path.u8string());

        std::vector<std::string> bashrc_content = maybe_bashrc_content.value_or_exit(VCPKG_LINE_INFO);

        std::vector<std::string> matches;
        for (auto&& line : bashrc_content)
        {
            std::smatch match;
            if (std::regex_match(line, match, std::regex{R"###(^source.*scripts/vcpkg_completion.bash$)###"}))
            {
                matches.push_back(line);
            }
        }

        if (!matches.empty())
        {
            System::printf("vcpkg bash completion is already imported to your %s file.\n"
                           "The following entries were found:\n"
                           "    %s\n"
                           "Please make sure you have started a new bash shell for the changes to take effect.\n",
                           bashrc_path.u8string(),
                           Strings::join("\n    ", matches));
            Checks::exit_success(VCPKG_LINE_INFO);
        }

        System::printf("Adding vcpkg completion entry to %s\n", bashrc_path.u8string());
        bashrc_content.push_back(Strings::format("source %s", completion_script_path.u8string()));
        fs.write_contents(bashrc_path, Strings::join("\n", bashrc_content) + '\n', VCPKG_LINE_INFO);
        Checks::exit_success(VCPKG_LINE_INFO);
    }
#endif

#if defined(_WIN32)
    const char* const INTEGRATE_COMMAND_HELPSTRING =
        "  vcpkg integrate install         Make installed packages available user-wide. Requires admin privileges on "
        "first use\n"
        "  vcpkg integrate remove          Remove user-wide integration\n"
        "  vcpkg integrate project         Generate a referencing nuget package for individual VS project use\n"
        "  vcpkg integrate powershell      Enable PowerShell tab-completion\n";
#else
    const char* const INTEGRATE_COMMAND_HELPSTRING =
        "  vcpkg integrate install         Make installed packages available user-wide.\n"
        "  vcpkg integrate remove          Remove user-wide integration\n"
        "  vcpkg integrate bash            Enable bash tab-completion\n";
#endif

    namespace Subcommand
    {
        static const std::string INSTALL = "install";
        static const std::string REMOVE = "remove";
        static const std::string PROJECT = "project";
        static const std::string POWERSHELL = "powershell";
        static const std::string BASH = "bash";
    }

    static std::vector<std::string> valid_arguments(const VcpkgPaths&)
    {
        return
        {
            Subcommand::INSTALL, Subcommand::REMOVE,
#if defined(_WIN32)
                Subcommand::PROJECT, Subcommand::POWERSHELL,
#else
                Subcommand::BASH,
#endif
        };
    }

    const CommandStructure COMMAND_STRUCTURE = {
        Strings::format("Commands:\n"
                        "%s",
                        INTEGRATE_COMMAND_HELPSTRING),
        1,
        1,
        {},
        &valid_arguments,
    };

    void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths)
    {
        Util::unused(args.parse_arguments(COMMAND_STRUCTURE));

        if (args.command_arguments[0] == Subcommand::INSTALL)
        {
            return integrate_install(paths);
        }
        if (args.command_arguments[0] == Subcommand::REMOVE)
        {
            return integrate_remove(paths.get_filesystem());
        }
#if defined(_WIN32)
        if (args.command_arguments[0] == Subcommand::PROJECT)
        {
            return integrate_project(paths);
        }
        if (args.command_arguments[0] == Subcommand::POWERSHELL)
        {
            return integrate_powershell(paths);
        }
#else
        if (args.command_arguments[0] == Subcommand::BASH)
        {
            return integrate_bash(paths);
        }
#endif

        Checks::exit_with_message(VCPKG_LINE_INFO, "Unknown parameter %s for integrate", args.command_arguments[0]);
    }
}
